C#实现实时查询股票信息的MCP Server
本篇使用的是新浪财经网页的股票查询接口,仅供学习研究演示之用。
什么是MCP
MCP 是由 Anthropic 于 2024 年 11 月 推出的开源协议,旨在为大型语言模型(LLM)提供统一的接口标准,实现与外部数据源、工具及服务的无缝连接。它被类比为 “AI 领域的 USB-C 接口” 或 “万能插头”,目标是解决传统 AI 集成中的碎片化问题,降低开发成本,提升实时性与安全性。
实战
调用股票查询接口
创建.Net 9项目
SinaStockClient.cs
public partial class SinaStockClient { private const string SINA_API_BASE = "https://hq.sinajs.cn/list"; private static readonly HttpClient client = new(); static SinaStockClient() { client.DefaultRequestHeaders.Referrer = new Uri("https://finance.sina.com.cn"); } public static async Task<StockQuote?> MakeStockRequest(string symbol) { var url = $"{SINA_API_BASE}={symbol}"; try { var response = await client.GetByteArrayAsync(url); Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); var text = Encoding.GetEncoding("GB2312").GetString(response); return ParseSinaStockData(text, symbol); } catch (Exception ex) { Console.WriteLine($"Error making stock request: {ex.Message}"); return null; } } private static StockQuote? ParseSinaStockData(string text, string symbol) { var match = StockRegex().Match(text); if (!match.Success) return null; var values = match.Groups[1].Value.Split(','); if (values.Length < 32) return null; return new StockQuote { Symbol = symbol, Name = values[0], Open = values[1], Close = values[2], Price = values[3], High = values[4], Low = values[5], Volume = values[8], Amount = values[9], Date = values[30], Time = values[31] }; } public static string FormatQuote(StockQuote quote) { if (quote == null) return "Invalid quote data"; var changeRate = (Convert.ToDouble(quote.Price) - Convert.ToDouble(quote.Close)) / Convert.ToDouble(quote.Close) * 100; return string.Join("\n", "---", $"Stock Code: {quote.Symbol}", $"Stock Name: {quote.Name}", $"Current Price: ¥{quote.Price}", $"Change Rate: {changeRate:F2}%", $"Open Price: ¥{quote.Open}", $"High Price: ¥{quote.High}", $"Low Price: ¥{quote.Low}", $"Volume: {Convert.ToDouble(quote.Volume) / 100:F0} lots", $"Turnover: ¥{Convert.ToDouble(quote.Amount) / 10000:F2} million", $"Update Time: {quote.Date} {quote.Time}", "---" ); } [GeneratedRegex("\"(.*)\"")] private static partial Regex StockRegex(); } public record StockQuote { public string? Symbol { get; set; } public string? Name { get; set; } public string? Open { get; set; } public string? Close { get; set; } public string? Price { get; set; } public string? High { get; set; } public string? Low { get; set; } public string? Volume { get; set; } public string? Amount { get; set; } public string? Date { get; set; } public string? Time { get; set; } }
调用测试
var quote = await SinaStockClient.MakeStockRequest("sh600000"); if (quote != null) { Console.WriteLine(SinaStockClient.FormatQuote(quote)); }
实现MCP Server
安装NuGet包:
dotnet add package ModelContextProtocol.AspNetCore
改造一下
SinaStockClient.cs
,以适应DI注入private readonly HttpClient client; public SinaStockClient(IHttpClientFactory ClientFactory) { client = ClientFactory.CreateClient(); client.DefaultRequestHeaders.Referrer = new Uri("https://finance.sina.com.cn"); }
定义MCP工具类
[McpServerToolType] public static class StockTool { [McpServerTool] [Description("Get real-time stock quote")] public static async Task<string> GetQuote(IHttpClientFactory ClientFactory, [Description("Stock symbol (e.g.: sh600000, sz000001)")] string symbol) { SinaStockClient stockClient = new(ClientFactory); var quote = await stockClient.MakeStockRequest(symbol); if (quote != null) return stockClient.FormatQuote(quote); return "没有找到这只股票的数据"; } }
在
Program.cs
中配置服务:builder.Services.AddHttpClient(); // 注册HttpClient builder.Services.AddMcpServer().WithHttpTransport().WithToolsFromAssembly(); // 自动注册工具,WithHttpTransport()为采用SSE模式返回MCP app.MapMcp(); // 映射MCP端点
生产小技巧:如果需要整合在已有的
Asp.Net Core
或Blazor Server
项目中,可以这样映射端点,避免路由冲突错误app.MapMcp("mcp"); // 映射MCP端点
在
Microsoft.SemanticKernel.Connectors.Ollama
中使用MCP Server,部分关键代码:private const string MCP_SERVER = "https://localhost:44328/mcp/sse"; // MCP Server的url,由于我是整合在现有项目中所有多了一个/mcp private readonly ChatHistory history = []; private IMcpClient? mcpClient; [Inject] private Kernel? Kernel { get; set; } // Enable automatic function calling private static readonly OpenAIPromptExecutionSettings executionSettings = new() { Temperature = 0, FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(options: new() { RetainArgumentTypes = true }) }; // 创建一个 SSE(Server-Sent Events)客户端传输配置实例 var config = new SseClientTransport( // 配置传输选项,指定服务端点(Endpoint) new SseClientTransportOptions() { // 设置远程服务器的 URI 地址 (记得替换真实的地址,从魔搭MCP广场获取) Endpoint = new Uri(MCP_SERVER) } ); mcpClient ??= await McpClientFactory.CreateAsync(config); // 调用客户端的 ListToolsAsync 方法,获取可用工具列表 var listToolsResult = await mcpClient.ListToolsAsync(); Kernel!.Plugins.AddFromFunctions("MyMCP", listToolsResult.Select(aiFunction => aiFunction.AsKernelFunction())); history.AddUserMessage(prompt); var ollamaChatClient = new OllamaApiClient(OllamaUri, modelname + ":latest"); client = new ChatClientBuilder(ollamaChatClient) // 👇🏼 Add function invocation to the chat client, wrapping the Ollama client .UseFunctionInvocation() .Build(); chat = client.AsChatCompletionService(); await foreach (var resp in chat.GetStreamingChatMessageContentsAsync(history, executionSettings: executionSettings, kernel: Kernel)) { Answer.Response += resp.Content; StateHasChanged(); }
最终运行效果图